﻿Xrm = window.Xrm || { __namespace: true };
Xrm.UndoShared = Xrm.UndoShared || { __namespace: true };

Xrm.UndoShared.CreatedDate = null;
Xrm.UndoShared.SchemaName = null;
Xrm.UndoShared.AuditDetail = [];

//Does the date formatting for display
Xrm.UndoShared.FormatDisplayDate = function (date, type) {
    if (type === "dt")
        return (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear() + " " +
            ("0" + date.getHours()).slice(-2) + ":" + ("0" + date.getMinutes()).slice(-2);
    else
        return (date.getMonth() + 1) + "/" + date.getDate() + "/" + date.getFullYear();
};

//Builds the content of the audit detail table
Xrm.UndoShared.DisplayAuditDetail = function (auditDetail, id, callback) {
    var data = [];
    for (var i = 0; i < auditDetail.length; i++) {
        for (var j = 0; j < auditDetail[i].changes.length; j++) {
            data.push({
                "revert": "<a class='undo' href='#' title='Undo' onclick='Xrm.UndoShared.Revert(\"" + auditDetail[i].changes[j].schemaName + "\", \"" + auditDetail[i].id +
                    "\", \"" + auditDetail[i].changes[j].precision + "\", \"" + id + "\", " + callback + ")'></a>",
                "changedfield": auditDetail[i].changes[j].label,
                "oldvalue": Xrm.UndoShared.GetDisplayValue(auditDetail[i].changes[j].attributeType, auditDetail[i].changes[j].oldValue,
                    auditDetail[i].changes[j].precision, auditDetail[i].changes[j].format),
                "newvalue": Xrm.UndoShared.GetDisplayValue(auditDetail[i].changes[j].attributeType, auditDetail[i].changes[j].newValue,
                    auditDetail[i].changes[j].precision, auditDetail[i].changes[j].format),
                "by": auditDetail[i].username,
                "date": Xrm.UndoShared.FormatDisplayDate(new Date(new Date(auditDetail[i].createdon).toLocaleString()), "dt")
            });
        }
    }

    var dataSource = new kendo.data.DataSource({
        data: data,
        pageSize: 5
    });

    $("#AuditDetail").kendoGrid({
        dataSource: dataSource,
        height: 190,
        pageable: {
            buttonCount: 5
        },
        columns: [
            {
                field: "revert",
                title: "&nbsp;",
                width: 30,
                encoded: false
            }, {
                field: "changedfield",
                title: "Changed Field"
            }, {
                field: "oldvalue",
                title: "Old Value"
            }, {
                field: "newvalue",
                title: "New Value"
            }, {
                field: "by",
                title: "By"
            }, {
                field: "date",
                title: "Date"
            }
        ]
    });
};

//Handles displaying the values in the table
Xrm.UndoShared.GetDisplayValue = function (attributeType, value, precision, format) {
    switch (attributeType) {
        case "Boolean":
        case "Picklist":
            return (value === null || value === undefined) ? "" : value.Name;
        case "Customer":
        case "Lookup":
            return (value === null || value === undefined) ? "" : value.Name;
        case "String":
        case "Memo":
            return (value === null) ? "" : value;
        case "Decimal":
        case "Double":
        case "Integer":
        case "Money":
            return (value === null) ? "" : parseFloat(value).toFixed(precision);
        case "DateTime":
            if (value === null) return "";
            var date = new Date(value);
            date = Xrm.UndoShared.FormatDisplayDate(date, (format === "DateAndTime") ? "dt" : "d");
            return date;
    }
    return "";
};

//Display a message when no data is available
Xrm.UndoShared.DisplayNoDataMessage = function (element, message) {
    var html = '<span id=\'message\'>';
    html += message;
    html += '</span>';
    $(element).html(html);
};

//Make the SOAP call for the audit detail
Xrm.UndoShared.GetAuditData = function (id, entity) {
    var auditDetails = null;

    var request = [
        "<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">",
          "<s:Body>",
            "<Execute xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">",
              "<request i:type=\"b:RetrieveRecordChangeHistoryRequest\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\" xmlns:b=\"http://schemas.microsoft.com/crm/2011/Contracts\">",
                "<a:Parameters xmlns:c=\"http://schemas.datacontract.org/2004/07/System.Collections.Generic\">",
                  "<a:KeyValuePairOfstringanyType>",
                    "<c:key>Target</c:key>",
                    "<c:value i:type=\"a:EntityReference\">",
                      "<a:Id>" + id + "</a:Id>",
                      "<a:LogicalName>" + entity + "</a:LogicalName>",
                      "<a:Name i:nil=\"true\" />",
                    "</c:value>",
                  "</a:KeyValuePairOfstringanyType>",
                "</a:Parameters>",
                "<a:RequestId i:nil=\"true\" />",
                "<a:RequestName>RetrieveRecordChangeHistory</a:RequestName>",
              "</request>",
            "</Execute>",
          "</s:Body>",
        "</s:Envelope>"
    ];

    var context = Xrm.UndoShared.GetContext();
    var req = new XMLHttpRequest();
    req.open("POST", encodeURI(context.getClientUrl() + "/XRMServices/2011/Organization.svc/web"), false);
    req.setRequestHeader("Accept", "application/xml, text/xml, */*");
    req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
    req.onreadystatechange = function () {
        if (req.readyState === 4) {
            if (req.status === 200) {
                var doc = $.parseXML(req.responseText.replace(/<(\/?)([^:>\s]*:)?([^>]+)>/g, "<$1$3>")); //Remove element namespaces
                auditDetails = $(doc).find("AuditDetail");
            } else {
                Xrm.UndoShared.DisplayNoDataMessage("#AuditDetail", "Error retrieving audit data or you are missing the View Audit History priviledge");
            }
        }
    };
    req.send(request.join(""));

    return auditDetails;
};

//Parse the SOAP response
Xrm.UndoShared.ObjectifyResults = function (auditDetails) {
    var parsedAuditDetails = [];

    for (var i = 0; i < auditDetails.length; i++) {
        var oldValues = [];
        var newValues = [];

        var record = {};
        record.id = $(auditDetails[i]).find("AuditRecord > Id").text();
        record.createdon = $(auditDetails[i]).find("AuditRecord > Attributes > KeyValuePairOfstringanyType:contains('createdon') > value").text();
        record.username = $(auditDetails[i]).find("AuditRecord > Attributes > KeyValuePairOfstringanyType:contains('userid') > value > Name").text();
        record.entity = $(auditDetails[i]).find("AuditRecord > Attributes > KeyValuePairOfstringanyType:contains('objectid') > value > LogicalName").text();

        var newKeys = $(auditDetails[i]).find("NewValue > Attributes > KeyValuePairOfstringanyType > key");
        var newVals = $(auditDetails[i]).find("NewValue > Attributes > KeyValuePairOfstringanyType > value");

        for (var j = 0; j < newVals.length; j++) {
            var newValue = {};
            newValue.fieldName = $(newKeys[j]).text();
            newValue.value = Xrm.UndoShared.GetValue(newVals[j]);
            newValues.push(newValue);
        }

        var oldKeys = $(auditDetails[i]).find("OldValue > Attributes > KeyValuePairOfstringanyType > key");
        var oldVals = $(auditDetails[i]).find("OldValue > Attributes > KeyValuePairOfstringanyType > value");

        for (var k = 0; k < oldVals.length; k++) {
            var oldValue = {};
            oldValue.fieldName = $(oldKeys[k]).text();
            oldValue.value = Xrm.UndoShared.GetValue(oldVals[k]);
            oldValues.push(oldValue);
        }
        record.changes = Xrm.UndoShared.UnifyValues(newValues, oldValues);

        parsedAuditDetails.push(record);
    }

    return parsedAuditDetails;
};

//Match up the new and old values
Xrm.UndoShared.UnifyValues = function (newVals, oldVals) {
    var changes = [];
    for (var i = 0; i < newVals.length; i++) {
        var val1 = {};
        val1.fieldName = newVals[i].fieldName;
        val1.newValue = newVals[i].value;
        for (var j = 0; j < oldVals.length; j++) {
            if (newVals[i].fieldName === oldVals[j].fieldName) {
                val1.oldValue = oldVals[j].value;
                break;
            }
        }
        if (val1.oldValue === null || val1.oldValue === undefined) {
            val1.oldValue = null;
        }

        //Remove items where old & new are the same
        if (JSON.stringify(val1.oldValue) !== JSON.stringify(val1.newValue)) {
            changes.push(val1);
        }
    }

    var match;
    for (var k = 0; k < oldVals.length; k++) {
        match = false;
        for (var l = 0; l < newVals.length; l++) {
            if (oldVals[k].fieldName === newVals[l].fieldName) {
                match = true;
                break;
            }
        }

        if (!match) {
            var val2 = {};
            val2.fieldName = oldVals[k].fieldName;
            val2.newValue = null;
            val2.oldValue = oldVals[k].value;
            //Remove items where old & new are the same
            if (JSON.stringify(val2.oldValue) !== JSON.stringify(val2.newValue)) {
                changes.push(val2);
            }
        }
    }

    return changes;
};

//Get the value from the SOAP response
Xrm.UndoShared.GetValue = function (node) {
    switch ($(node).attr("i:type")) {
        case "d:dateTime":
        case "d:decimal":
        case "d:double":
        case "d:int":
        case "d:string":
            return $(node).text();
        case "d:boolean":
            var bVal = {};
            bVal.Value = $(node).text();
            return bVal;
        case "a:OptionSetValue":
            var oVal = {};
            oVal.Value = $(node).find("Value").text();
            return oVal;
        case "a:Money":
            return $(node).find("Value").text();
        case "a:EntityReference":
            var eVal = {};
            eVal.Id = $(node).find("Id").text();
            eVal.LogicalName = $(node).find("LogicalName").text();
            eVal.Name = $(node).find("Name").text();
            return eVal;
    }
    return null;
};

//Get the entity and field metadata we need to make this all work
Xrm.UndoShared.GetMetadata = function (auditDetail, id, entity, callback) {
    var fields = Xrm.UndoShared.GetFields(auditDetail);

    var mdq = SDK.Metadata.Query;
    var semp = mdq.SearchableEntityMetadataProperties;
    var samp = mdq.SearchableAttributeMetadataProperties;
    var emp = mdq.EntityMetadataProperties;
    var amp = mdq.AttributeMetadataProperties;

    var entityFilter = new mdq.MetadataFilterExpression(mdq.LogicalOperator.And);
    entityFilter.addCondition(semp.LogicalName, mdq.MetadataConditionOperator.Equals, entity);
    var entityProperties = new mdq.MetadataPropertiesExpression(false, [emp.Attributes, emp.SchemaName]);
    var attributesFilter = new mdq.MetadataFilterExpression(mdq.LogicalOperator.Or);
    attributesFilter.addCondition(samp.LogicalName, mdq.MetadataConditionOperator.In, fields);
    var attributeProperties = new mdq.MetadataPropertiesExpression(false, [amp.DisplayName, amp.AttributeType, amp.OptionSet, amp.SchemaName, amp.IsValidForUpdate, amp.Precision, amp.Format]);
    var labelQuery = new mdq.LabelQueryExpression([window.parent.Xrm.Page.context.getUserLcid()]);
    var query = new mdq.EntityQueryExpression(entityFilter, entityProperties, new mdq.AttributeQueryExpression(attributesFilter, attributeProperties), null, labelQuery);
    var request = new mdq.RetrieveMetadataChangesRequest(query);
    mdq.RetrieveMetadataChanges(request,
        function (retrieveMetadataResponse) {
            Xrm.UndoShared.SchemaName = retrieveMetadataResponse.EntityMetadata[0].SchemaName;
            Xrm.UndoShared.GetCreatedDate(id);
            var attributes = retrieveMetadataResponse.EntityMetadata[0].Attributes;

            for (var i = 0; i < attributes.length; i++) {
                for (var j = 0; j < auditDetail.length; j++) {
                    for (var k = 0; k < auditDetail[j].changes.length; k++) {

                        //Don't include the audit detail if it is from when the record was first created
                        if (new Date(auditDetail[j].createdon).toISOString() === new Date(Xrm.UndoShared.CreatedDate).toISOString()) {
                            auditDetail[j].changes.splice(k, 1);
                            continue;
                        }

                        if (attributes[i].LogicalName === auditDetail[j].changes[k].fieldName) {
                            //Don't include the audit detail if it is type we aren't going to handle rolling back
                            if (attributes[i].AttributeType === "State" || attributes[i].AttributeType === "Status" || attributes[i].AttributeType === "Uniqueidentifier") {
                                auditDetail[j].changes.splice(k, 1);
                                continue;
                            }

                            if (attributes[i].IsValidForUpdate === false) {
                                auditDetail[j].changes.splice(k, 1);
                                continue;
                            }

                            auditDetail[j].changes[k].schemaName = attributes[i].SchemaName;
                            auditDetail[j].changes[k].attributeType = attributes[i].AttributeType;
                            auditDetail[j].changes[k].precision = attributes[i].Precision;
                            auditDetail[j].changes[k].format = attributes[i].Format;
                            var label = "[No Label]";
                            if ((attributes[i].DisplayName.UserLocalizedLabel !== null) && (attributes[i].DisplayName.UserLocalizedLabel.Label !== null)) {
                                label = attributes[i].DisplayName.UserLocalizedLabel.Label;
                            }
                            auditDetail[j].changes[k].label = label;

                            if (attributes[i].AttributeType === "Picklist") {
                                auditDetail[j].changes[k].options = attributes[i].OptionSet.Options;
                                if (auditDetail[j].changes[k].newValue !== null) {
                                    auditDetail[j].changes[k].newValue.Name =
                                        Xrm.UndoShared.GetOptionsetLabel(auditDetail[j].changes[k].options, auditDetail[j].changes[k].newValue.Value);
                                }
                                if (auditDetail[j].changes[k].oldValue !== null) {
                                    auditDetail[j].changes[k].oldValue.Name =
                                        Xrm.UndoShared.GetOptionsetLabel(auditDetail[j].changes[k].options, auditDetail[j].changes[k].oldValue.Value);
                                }
                            }
                            if (attributes[i].AttributeType === "Boolean") {
                                auditDetail[j].changes[k].optionSet = attributes[i].OptionSet;
                                if (auditDetail[j].changes[k].newValue !== null) {
                                    auditDetail[j].changes[k].newValue.Name =
                                        auditDetail[j].changes[k].newValue.Name =
                                        Xrm.UndoShared.GetBooleanLabel(
                                        ((auditDetail[j].changes[k].newValue.Value === "true") ? auditDetail[j].changes[k].optionSet.TrueOption :
                                            auditDetail[j].changes[k].optionSet.FalseOption));
                                }
                                if (auditDetail[j].changes[k].oldValue !== null) {
                                    auditDetail[j].changes[k].oldValue.Name =
                                        Xrm.UndoShared.GetBooleanLabel(
                                        ((auditDetail[j].changes[k].oldValue.Value === "true") ? auditDetail[j].changes[k].optionSet.TrueOption :
                                            auditDetail[j].changes[k].optionSet.FalseOption));
                                }
                            }
                        }
                    }
                }
            }

            callback(auditDetail);
        },
        function (error) {
            alert(error.message);
        });
};

//Retrieves the created date of the record
Xrm.UndoShared.GetCreatedDate = function (id) {
    var context = Xrm.UndoShared.GetContext();
    var req = new XMLHttpRequest();
    req.open("GET", encodeURI(context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/" + Xrm.UndoShared.SchemaName + "Set(guid'" + id + "')?$select=CreatedOn"), false);
    req.setRequestHeader("Accept", "application/json");
    req.setRequestHeader("Content-Type", "application/json;charset=utf-8");
    req.onreadystatechange = function () {
        if (req.readyState === 4) {
            if (req.status === 200) {
                var retrieved = JSON.parse(req.responseText).d;
                Xrm.UndoShared.CreatedDate = new Date(parseInt(retrieved.CreatedOn.substr(6))).toUTCString();
            }
        }
    };
    req.send();
};

//Gets the list of unique fields from the audit detail we need to get metadata on
Xrm.UndoShared.GetFields = function (auditDetail) {
    var fields = [];
    for (var i = 0; i < auditDetail.length; i++) {
        for (var j = 0; j < auditDetail[i].changes.length; j++) {
            fields.push(auditDetail[i].changes[j].fieldName);
        }
    }

    return Xrm.UndoShared.RemoveArrayDuplicates(fields);
};

Xrm.UndoShared.RemoveArrayDuplicates = function (input) {
    var i,
      len = input.length,
      out = [],
      obj = {};

    for (i = 0; i < len; i++) {
        obj[input[i]] = 0;
    }
    for (i in obj) {
        out.push(i);
    }
    return out;
};

//Gets the text value of an option set
Xrm.UndoShared.GetOptionsetLabel = function (options, value) {
    if (options === null || options === undefined) return null;

    for (var i = 0; i < options.length; i++) {
        if (options[i].Value === parseInt(value)) {
            var option = options[i];
            if (option.Label === null) {
                return "[No Label]";
            }
            else {
                if (option.Label.UserLocalizedLabel !== null) {
                    return option.Label.UserLocalizedLabel.Label;
                } else {
                    for (var j = 0; j < option.Label.LocalizedLabels.length; j++) {
                        if (option.Label.LocalizedLabels[j].LanguageCode === Xrm.Page.context.getUserLcid()) {
                            return option.Label.LocalizedLabels[j].Label;
                        }
                    }
                }
            }
        }
    }
    return "[No Label]";
};

//Gets the text values of a boolean field
Xrm.UndoShared.GetBooleanLabel = function (option) {
    if (option === null || option === undefined) return "[No Label]";
    if (option.Label === null) {
        return "[No Label]";
    }
    else {
        if (option.Label.UserLocalizedLabel !== null) {
            return option.Label.UserLocalizedLabel.Label;
        } else {
            for (var j = 0; j < option.Label.LocalizedLabels.length; j++) {
                if (option.Label.LocalizedLabels[j].LanguageCode === Xrm.Page.context.getUserLcid()) {
                    return option.Label.LocalizedLabels[j].Label;
                }
            }
        }
    }
    return "[No Label]";
};

//Reverts back to a previous value
Xrm.UndoShared.Revert = function (schemaName, auditId, precision, recordId, callback) {
    var auditDetail = $.grep(Xrm.UndoShared.AuditDetail, function (e) { return e.id == auditId; });
    var change = $.grep(auditDetail[0].changes, function (e) { return e.schemaName == schemaName; });
    var record = {};
    record[change[0].schemaName] = null;
    switch (change[0].attributeType) {
        case "Picklist":
            record[change[0].schemaName] = (change[0].oldValue === null) ? null :
            {
                Value: change[0].oldValue.Value
            };
            break;
        case "Customer":
        case "Lookup":
            record[change[0].schemaName] = (change[0].oldValue === null) ? null :
                {
                    __metadata: {
                        type: "Microsoft.Crm.Sdk.Data.Services.EntityReference"
                    },
                    Id: change[0].oldValue.Id,
                    LogicalName: change[0].oldValue.LogicalName
                };
            break;
        case "Boolean":
            record[change[0].schemaName] = (change[0].oldValue === null) ? null : change[0].oldValue.Value;
            break;
        case "String":
        case "Memo":
        case "Integer":
            record[change[0].schemaName] = (change[0].oldValue === null) ? null : change[0].oldValue;
            break;
        case "Decimal":
        case "Double":
            record[change[0].schemaName] = (change[0].oldValue === null) ? null : parseFloat(change[0].oldValue).toFixed(precision);
            break;
        case "Money":
            record[change[0].schemaName] = (change[0].oldValue === null) ? null :
             {
                 Value: parseFloat(change[0].oldValue).toFixed(precision)
             };
            break;
        case "DateTime":
            var date = new Date(change[0].oldValue);
            record[change[0].schemaName] = (change[0].oldValue === null) ? null : date.toLocaleString();
            break;
        default:
            return;
    }

    var context = Xrm.UndoShared.GetContext();
    var req = new XMLHttpRequest();
    req.open("POST", encodeURI(context.getClientUrl() + "/XRMServices/2011/OrganizationData.svc/" + Xrm.UndoShared.SchemaName + "Set(guid'" + recordId + "')"), true);
    req.setRequestHeader("Accept", "application/json");
    req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
    req.setRequestHeader("X-HTTP-Method", "MERGE");
    req.onreadystatechange = function () {
        if (this.readyState === 4) {
            if (this.status === 204 || this.status === 1223) {
                callback();
            }
        }
    };
    req.send(JSON.stringify(record));
};

Xrm.UndoShared.GetContext = function () {
    var errorMessage = "Context is not available.";
    if (typeof GetGlobalContext != "undefined") {
        return GetGlobalContext();
    }
    else {
        if (typeof Xrm != "undefined") {
            return Xrm.Page.context;
        } else {
            throw new Error(errorMessage);
        }
    }
};